package de.fhkl.strokeprotocol.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.fhkl.strokeprotocol.listeners.ICloseListener;
import de.fhkl.strokeprotocol.listeners.IMessageListener;
import de.fhkl.strokeprotocol.messages.IMessage;

/**
 * StrokeInputReader listens at a given stream and dispatches the received
 * messages to the correspondent listeners.
 */
public class StrokeInputReader
{
	/** Init the logger for this class */
	private static Logger sLogger = LoggerFactory.getLogger(StrokeInputReader.class);

	/** Stream where StrokeInputReader should listen for messages */
	private ObjectInputStream mIn;

	/** Listeners for all known messages */
	private Map<Class<? extends IMessage>, IMessageListener> mListeners;

	/** Thread in which we run our listener */
	private Thread mThread;

	/** Indicates if the thread should run, allows us to stop this listener */
	private boolean mRun;

	/** Listener for when StrokeInputReader is closed */
	private ICloseListener mCloseListener;

	/**
	 * StrokeInputReader listens at a given stream and dispatches the received
	 * messages to the correspondent listeners.
	 * 
	 * @param in
	 *            stream to listen for incoming messages
	 * @throws IOException
	 */
	public StrokeInputReader(InputStream in) throws IOException
	{
		mIn = new ObjectInputStream(in);
		mListeners = new HashMap<Class<? extends IMessage>, IMessageListener>();
	}

	/**
	 * Starts the listener in a new thread and waits for new incoming messages
	 * and dispatches them to the listeners registered for that class of
	 * message.
	 * 
	 * @param closeListener
	 *            ICloseListener that is informed when StrokeInputReader is
	 *            closed, null if you don't want to be informed
	 */
	public void start(ICloseListener closeListener)
	{
		mRun = true;
		mCloseListener = closeListener;

		mThread = new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				// listen for incoming messages and dispatch them to the
				// correspondent listeners
				while (mRun)
				{
					try
					{
						IMessage message = (IMessage) mIn.readObject();

						if (message != null)
						{
							// dispatch messages to the correspondent listeners
							IMessageListener listener = mListeners.get(message.getClass());

							if (listener != null)
							{
								listener.onMessage(message);
							}
							else
							{
								sLogger.warn("Received an unknown message.");
							}
						}
					}
					catch (ClassCastException e)
					{
						sLogger.error("Received broken message. " + e.getMessage());
					}
					catch (Exception e)
					{
						if (mRun)
						{
							sLogger.error("Connection failure: " + e.getMessage());
							sLogger.info("StrokeInputReader stopped.");

							// inform listener that StrokeInputReader has been
							// stopped.
							if (mCloseListener != null)
							{
								mCloseListener.onClose(this);
							}

							// stop input reader
							mRun = false;
						}
					}
				}
			}
		});

		mThread.start();
	}

	/**
	 * Stops the listener thread.
	 */
	public void stop()
	{
		try
		{
			if (mRun)
			{
				sLogger.info("Stopping StrokeInputReader...");

				mRun = false;

				mIn.close();
				mIn = null;

				mThread = null;

				sLogger.info("StrokeInputReader successfully stopped.");
			}
			else
			{
				sLogger.info("StrokeInputReader already stopped.");
			}
		}
		catch (Exception e)
		{
			sLogger.error(e.getClass().getSimpleName() + ": " + e.getMessage());
		}
	}

	/**
	 * Allows to register a listener for incoming messages of the given class.
	 * 
	 * @param messageType
	 * @param listener
	 */
	public void setMessageListener(Class<? extends IMessage> messageType, IMessageListener listener)
	{
		mListeners.put(messageType, listener);
	}
}
